<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SVG Grid Layout</title>
  <style>
    #canvas {
      border: 1px solid #ccc;
      background-color: #f9f9f9;
    }
    .node {
      cursor: move;
    }
    .connector {
      cursor: pointer;
    }
    .selected {
      stroke: red;
    }
  </style>
</head>
<body>
  <button id="addNode">Add Node</button>
  <svg id="canvas" width="800" height="600"></svg>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const svgNS = 'http://www.w3.org/2000/svg';
      const canvas = document.getElementById('canvas');
      const addNodeButton = document.getElementById('addNode');

      const gridSize = 20; // 定义网格大小

      let nodes = [];
      let links = [];
      let selectedLink = null;

      // 画网格线
      function drawGrid() {
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;

        for (let x = gridSize; x < width; x += gridSize) {
          const line = document.createElementNS(svgNS, 'line');
          line.setAttribute('x1', x);
          line.setAttribute('y1', 0);
          line.setAttribute('x2', x);
          line.setAttribute('y2', height);
          line.setAttribute('stroke', '#e0e0e0');
          line.setAttribute('stroke-width', 1);
          canvas.appendChild(line);
        }

        for (let y = gridSize; y < height; y += gridSize) {
          const line = document.createElementNS(svgNS, 'line');
          line.setAttribute('x1', 0);
          line.setAttribute('y1', y);
          line.setAttribute('x2', width);
          line.setAttribute('y2', y);
          line.setAttribute('stroke', '#e0e0e0');
          line.setAttribute('stroke-width', 1);
          canvas.appendChild(line);
        }
      }

      drawGrid();

      addNodeButton.addEventListener('click', addNode);

      function addNode() {
        const newNode = createNode();
        canvas.appendChild(newNode);
        nodes.push(newNode);
        makeDraggable(newNode);
      }

      function createNode() {
        const group = document.createElementNS(svgNS, 'g');
        group.setAttribute('data-id', `node-${nodes.length + 1}`);
        const rect = document.createElementNS(svgNS, 'rect');
        rect.setAttribute('x', 50);
        rect.setAttribute('y', 50);
        rect.setAttribute('width', 100);
        rect.setAttribute('height', 50);
        rect.setAttribute('fill', '#007BFF');
        rect.setAttribute('stroke', '#000');

        const connectors = ['top', 'bottom', 'left', 'right'].map(position => createConnector(position));

        connectors.forEach(connector => group.appendChild(connector));
        group.appendChild(rect);
        group.classList.add('node');
        group.setAttribute('transform', 'translate(0, 0)');

        return group;
      }

      function createConnector(position) {
        const circle = document.createElementNS(svgNS, 'circle');
        circle.setAttribute('r', 5);
        circle.setAttribute('fill', '#FFF');
        circle.setAttribute('stroke', '#000');
        circle.setAttribute('stroke-width', 2);
        circle.classList.add('connector');
        circle.dataset.position = position;

        switch (position) {
          case 'top':
            circle.setAttribute('cx', 100);
            circle.setAttribute('cy', 45);
            break;
          case 'bottom':
            circle.setAttribute('cx', 100);
            circle.setAttribute('cy', 105);
            break;
          case 'left':
            circle.setAttribute('cx', 45);
            circle.setAttribute('cy', 75);
            break;
          case 'right':
            circle.setAttribute('cx', 155);
            circle.setAttribute('cy', 75);
            break;
        }

        return circle;
      }

      function makeDraggable(node) {
        let offsetX, offsetY;
        let isDragging = false;
        const rect = node.querySelector('rect');

        rect.addEventListener('mousedown', (e) => {
          e.preventDefault();
          const transform = node.getAttribute('transform');
          const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
          const startX = translate ? parseFloat(translate[1]) : 0;
          const startY = translate ? parseFloat(translate[2]) : 0;
          offsetX = e.clientX - startX;
          offsetY = e.clientY - startY;
          isDragging = true;
          document.addEventListener('mousemove', onMouseMove);
          document.addEventListener('mouseup', onMouseUp);
        });

        function onMouseMove(e) {
          if (!isDragging) return;
          const newX = e.clientX - offsetX;
          const newY = e.clientY - offsetY;

          // 对齐到最近的网格点
          const snappedX = Math.round(newX / gridSize) * gridSize;
          const snappedY = Math.round(newY / gridSize) * gridSize;

          if (!isNaN(snappedX) && !isNaN(snappedY)) {
            node.setAttribute('transform', `translate(${snappedX}, ${snappedY})`);
            requestAnimationFrame(updateLinks);
          }
        }

        function onMouseUp() {
          isDragging = false;
          document.removeEventListener('mousemove', onMouseMove);
          document.removeEventListener('mouseup', onMouseUp);
        }
      }

      let startConnector = null;

      canvas.addEventListener('click', (e) => {
        if (e.target.classList.contains('connector')) {
          if (startConnector) {
            const endConnector = e.target;
            if (startConnector !== endConnector) {
              createLink(startConnector, endConnector);
              startConnector = null;
            }
          } else {
            startConnector = e.target;
          }
        } else if (e.target.tagName === 'path') {
          selectLink(e.target);
        }
      });

      canvas.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        if (e.target.tagName === 'path') {
          deleteLink(e.target);
        }
      });

      function createLink(startConnector, endConnector) {
        const path = document.createElementNS(svgNS, 'path');
        path.setAttribute('stroke', '#000');
        path.setAttribute('fill', 'none');
        path.dataset.startNodeId = startConnector.parentElement.dataset.id;
        path.dataset.startPosition = startConnector.dataset.position;
        path.dataset.endNodeId = endConnector.parentElement.dataset.id;
        path.dataset.endPosition = endConnector.dataset.position;
        canvas.appendChild(path);
        links.push(path);
        updateLinks();
      }

      function getConnectorCoords(connector) {
        const transform = connector.parentElement.getAttribute('transform');
        const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
        if (translate) {
          const parentX = parseFloat(translate[1]);
          const parentY = parseFloat(translate[2]);
          const cx = parseFloat(connector.getAttribute('cx'));
          const cy = parseFloat(connector.getAttribute('cy'));
          return { x: parentX + cx, y: parentY + cy };
        }
        return { x: 0, y: 0 };
      }

      function updateLinks() {
        links.forEach(link => {
          const startNode = document.querySelector(`[data-id="${link.dataset.startNodeId}"]`);
          const endNode = document.querySelector(`[data-id="${link.dataset.endNodeId}"]`);
          const startConnector = startNode.querySelector(`.connector[data-position="${link.dataset.startPosition}"]`);
          const endConnector = endNode.querySelector(`.connector[data-position="${link.dataset.endPosition}"]`);
          const startCoords = getConnectorCoords(startConnector);
          const endCoords = getConnectorCoords(endConnector);

          // 使用 findPath 函数计算避开节点的路径
          const pathData = findPath(startCoords, endCoords);
          link.setAttribute('d', pathData);
        });
      }

      // A*算法实现
      function findPath(start, end) {
        const openSet = [];
        const closedSet = [];
        const gridSize = 10;
        const grid = createGrid(gridSize);

        const startNode = createNodeFromCoords(start, gridSize);
        const endNode = createNodeFromCoords(end, gridSize);

        openSet.push(startNode);

        while (openSet.length > 0) {
          let lowestIndex = 0;
          for (let i = 0; i < openSet.length; i++) {
            if (openSet[i].f < openSet[lowestIndex].f) {
              lowestIndex = i;
            }
          }

          const current = openSet[lowestIndex];

          if (current.x === endNode.x && current.y === endNode.y) {
            return reconstructPath(current);
          }

          openSet.splice(lowestIndex, 1);
          closedSet.push(current);

          const neighbors = getNeighbors(current, grid);

          neighbors.forEach(neighbor => {
            if (closedSet.includes(neighbor) || isNodeObstructed(neighbor, gridSize)) {
              return;
            }

            const tentativeG = current.g + 1;

            if (!openSet.includes(neighbor)) {
              openSet.push(neighbor);
            } else if (tentativeG >= neighbor.g) {
              return;
            }

            neighbor.g = tentativeG;
            neighbor.h = heuristic(neighbor, endNode);
            neighbor.f = neighbor.g + neighbor.h;
            neighbor.cameFrom = current;
          });
        }

        return '';
      }

      function createGrid(size) {
        const grid = [];
        const cols = canvas.clientWidth / size;
        const rows = canvas.clientHeight / size;

        for (let x = 0; x < cols; x++) {
          const col = [];
          for (let y = 0; y < rows; y++) {
            col.push({ x, y, f: 0, g: 0, h: 0, cameFrom: null });
          }
          grid.push(col);
        }

        return grid;
      }

      function createNodeFromCoords(coords, gridSize) {
        return {
          x: Math.floor(coords.x / gridSize),
          y: Math.floor(coords.y / gridSize),
          f: 0,
          g: 0,
          h: 0,
          cameFrom: null
        };
      }

      function getNeighbors(node, grid) {
        const neighbors = [];
        const directions = [
          { x: 0, y: -1 },
          { x: 1, y: 0 },
          { x: 0, y: 1 },
          { x: -1, y: 0 }
        ];

        directions.forEach(direction => {
          const newX = node.x + direction.x;
          const newY = node.y + direction.y;

          if (newX >= 0 && newX < grid.length && newY >= 0 && newY < grid[0].length) {
            neighbors.push(grid[newX][newY]);
          }
        });

        return neighbors;
      }

      function heuristic(node, endNode) {
        return Math.abs(node.x - endNode.x) + Math.abs(node.y - endNode.y);
      }

      function reconstructPath(node) {
        let path = '';
        let current = node;

        while (current.cameFrom) {
          path = `L ${current.x * 10} ${current.y * 10} ` + path;
          current = current.cameFrom;
        }

        path = `M ${current.x * 10} ${current.y * 10} ` + path;

        return path;
      }

      function isNodeObstructed(node, gridSize) {
        for (const n of nodes) {
          const rect = n.querySelector('rect');
          const transform = n.getAttribute('transform');
          const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
          const rectX = parseFloat(translate[1]) + parseFloat(rect.getAttribute('x'));
          const rectY = parseFloat(translate[2]) + parseFloat(rect.getAttribute('y'));
          const rectWidth = parseFloat(rect.getAttribute('width'));
          const rectHeight = parseFloat(rect.getAttribute('height'));

          const minX = Math.floor(rectX / gridSize);
          const maxX = Math.floor((rectX + rectWidth) / gridSize);
          const minY = Math.floor(rectY / gridSize);
          const maxY = Math.floor((rectY + rectHeight) / gridSize);

          if (node.x >= minX && node.x <= maxX && node.y >= minY && node.y <= maxY) {
            return true;
          }
        }

        return false;
      }

      function selectLink(link) {
        if (selectedLink) {
          selectedLink.classList.remove('selected');
        }
        selectedLink = link;
        selectedLink.classList.add('selected');
      }

      function deleteLink(link) {
        canvas.removeChild(link);
        const index = links.indexOf(link);
        if (index !== -1) {
          links.splice(index, 1);
        }
        selectedLink = null;
      }
    });
  </script>
</body>
</html>
